---
title: Вращаем куб в пространстве
description: Рассмотрим разницу между параллельной и перспективной проекцией. Обе широко используются на практике для различных целей. В предыдущем примере мы вращали...
sections: [Линейная перспектива,Матрица поворота,Экспериментальная модель]
tags: [javascript,онлайн,canvas,геометрия,графика,изображение,картинка,квадрат,куб,3д,трёхмерный]
scripts: [/js/classes-point-cube.js,/js/spinning-cube.js,/js/spinning-cube2.js]
styles: [/css/pomodoro1.css]
canonical_url: /ru/2023/01/10/spinning-cube-in-space.html
url_translated: /en/2023/01/11/spinning-cube-in-space.html
title_translated: Spinning cube in space
date: 2023.01.10
---

Рассмотрим разницу между параллельной и перспективной проекцией. Обе широко
используются на практике для различных целей. В предыдущем примере мы
[вращали квадрат на плоскости]({{ '/ru/2023/01/05/spinning-square-on-plane.html' | relative_url }})
— переходим в трёхмерное пространство. Теперь, чтобы отобразить на плоскости экрана поворот трёхмерного
объекта, нужно сначала создать *математическую модель* трёхмерного объекта, повернуть её на угол, срисовать
с неё проекцию и отобразить на экране уже проекцию. Для наглядности будем использовать декартову систему координат.

Усложнённая модель, много кубиков: [Вращаем пространственный крест]({{ '/ru/2023/01/15/spinning-spatial-cross.html' | relative_url }}).

<div style="display: flex; flex-direction: row; flex-wrap: wrap;">
  <div style="display: flex; flex-direction: column; padding-right: 8px;">
    <span>Параллельная проекция</span>
    <canvas id="canvas1" width="300" height="300" style="border: 1px solid gray;">
      <p>Холст для отображения результатов вычислений</p>
    </canvas>
  </div>
  <div style="display: flex; flex-direction: column;">
    <span>Перспективная проекция</span>
    <canvas id="canvas2" width="300" height="300" style="border: 1px solid gray;">
      <p>Холст для отображения результатов вычислений</p>
    </canvas>
  </div>
</div>

*Параллельная проекция* — центр проекции бесконечно удалён от плоскости экрана наблюдателя,
размеры предметов выглядят одинаковыми.

*Перспективная проекция* — параллельные линии сходятся в центре перспективы, предметы выглядят
уменьшающимися вдалеке.

{% include heading.html text="Экспериментальная модель" hash="experimental-model" %}

Размер куба 200, размер холста 300, начало координат находится в верхнем левом углу. Центр фигуры
в середине холста. Ось `X` направлена вправо, ось `Y` направлена вниз, ось `Z` направлена вдаль.
Выполняется поворот последовательно по всем трём осям: сначала по оси `X`, затем по оси `Y` и затем
по оси `Z`. Настройками модели можно управлять, например можно отключать лишнее вращение по осям и
двигать центральную точку проекции на экране наблюдателя.

<div>
<canvas id="canvas3" width="300" height="300" style="border: 1px solid gray;">
  <p>Холст для отображения результатов вычислений</p>
</canvas>
</div>
<form>
<div>
  <span>Вращение по осям:</span>
  <input type="checkbox" id="rotateX" name="rotateX" checked onchange="changeAxis('x',event)">
  <label for="rotateX">X</label>
  <input type="checkbox" id="rotateY" name="rotateY" checked onchange="changeAxis('y',event)">
  <label for="rotateY">Y</label>
  <input type="checkbox" id="rotateZ" name="rotateZ" checked onchange="changeAxis('z',event)">
  <label for="rotateZ">Z</label>
</div>
<span>Центр на экране наблюдателя:</span>
<div>
  <input type="range" id="rangeX" name="rangeX" min="0" max="300" value="150" step="1"
         oninput="resultX.value = rangeX.valueAsNumber; changeTv('x',event)">
  <label for="rangeX">X</label>
  <output id="resultX" name="resultX">150</output>
</div>
<div>
  <input type="range" id="rangeY" name="rangeY" min="0" max="300" value="150" step="1"
         oninput="resultY.value = rangeY.valueAsNumber; changeTv('y',event)">
  <label for="rangeY">Y</label>
  <output id="resultY" name="resultY">150</output>
</div>
<div>
  <input type="range" id="rangeZ" name="rangeZ" min="50" max="200" value="60" step="1"
         oninput="resultZ.value = rangeZ.valueAsNumber; changeTv('z',event)">
  <label for="rangeZ">Z</label>
  <output name="resultZ">60</output>
</div>
<span>Удалённость центра проекции:</span>
<div>
  <input type="range" id="distance" name="distance" min="220" max="800" value="300" step="10"
         oninput="result.value = distance.valueAsNumber; changeDistance(event);">
  <label for="distance">Z</label>
  <output name="result">300</output>
</div>
<div>
  <label for="center">Двигать центральную точку:</label>
  <input type="checkbox" id="center" name="center" oninput="showCenter(event)">
</div>
</form>

{% include heading.html text="Поворот точки в пространстве" hash="point-rotation-in-space" %}

Рассчитываем новые координаты точки по формулам матрицы поворота для трёхмерного пространства.
Поворачиваем точку `t` относительно точки `t0` — получаем точку `t'`.

*Поворот по оси `X`.*

{% include image_svg.html src="/img/column-vector3dx.svg" style="width: 242.619pt; height: 59.0768pt;"
alt="&x'=x,&\\&y'=y_0+(y-y_0)cos\varphi-(z-z_0)sin\varphi,&\\&z'=z_0+(y-y_0)sin\varphi+(z-z_0)cos\varphi.&\\" %}

*Поворот по оси `Y`.*

{% include image_svg.html src="/img/column-vector3dy.svg" style="width: 246.251pt; height: 59.0768pt;"
alt="&x'=x_0+(x-x_0)cos\varphi-(z-z_0)sin\varphi,&\\&y'=y,&\\&z'=z_0+(x-x_0)sin\varphi+(z-z_0)cos\varphi.&\\" %}

*Поворот по оси `Z`.*

{% include image_svg.html src="/img/column-vector3dz.svg" style="width: 246.793pt; height: 55.4753pt;"
alt="&x'=x_0+(x-x_0)cos\varphi-(y-y_0)sin\varphi,&\\&y'=y_0+(x-x_0)sin\varphi+(y-y_0)cos\varphi,&\\&z'=z.&\\" %}

{% include heading.html text="Проекция точки" hash="point-projection" %}

Экспериментальные формулы с возможностью смещения центра проекции `d0` на экране наблюдателя `tv`.
Отображаем точку пространства `t` на плоскость экрана — получаем точку `t'`.

*Параллельная проекция.*

{% include image_svg.html src="/img/oblique-projection.svg" style="width: 123.97pt; height: 37.2836pt;"
alt="&x'=x,&\\&y'=y+(y_v-z)/4.&\\" %}

*Перспективная проекция.*

{% include image_svg.html src="/img/central-projection.svg" style="width: 231.924pt; height: 37.2836pt;"
alt="&x'=x_v+d_0\cdot(x-x_v)/(z-z_v+d_0),&\\&y'=y_v+d_0\cdot(y-y_v)/(z-z_v+d_0).&\\" %}

*Расстояние от точки до центра проекции.*

{% include image_svg.html src="/img/euclidean-distance.svg" style="width: 319.911pt; height: 17.9328pt;"
alt="d(t,d_0)=\sqrt{(x-x_v)^2+(y-y_v)^2+(z-z_v+d_0)^2}." %}

{% include heading.html text="Сортировка граней" hash="face-sorting" %}

При создании кубика, вершины каждой грани задаём по часовой стрелке. При получении проекции, подставляем
в уравнение прямой три подряд идущие вершины, чтобы определить наклон грани и удалённость её от плоскости
проекции.

*Уравнение прямой, проходящей через две точки.*

{% include image_svg.html src="/img/linear-equation.svg" style="width: 137.171pt; height: 35.3194pt;"
alt="{(x-x_1)\over(y-y_1)}={(x_2-x_1)\over(y_2-y_1)}." %}

{% include heading.html text="Описание алгоритма" hash="algorithm-description" %}

Сначала обходим вершины куба и поворачиваем их на угол относительно центральной точки. Затем обходим грани
куба и получаем проекции входящих в них вершин. После этого сортируем проекции граней по удалённости. Затем
рисуем проекции на плоскости — соединяем точки линиями. Рисуем почти прозрачным цветом сперва дальние грани
и поверх них ближние, чтобы сквозь ближние грани было видно дальние.

На каждом шаге отображения фигуры повторяем сортировку граней по удалённости, так как с изменением
угла поворота, координаты смещаются, и ближние грани становятся дальними.

{% include heading.html text="Реализация на JavaScript" hash="implementation-in-javascript" %}

{% include classes-point-cube-ru.md -%}

Создаём объект и рисуем две проекции на плоскости.

```js
'use strict';
// рисовать будем сразу две картинки,
// объект будет один, а проекций будет много
const canvas1 = document.getElementById('canvas1');
const canvas2 = document.getElementById('canvas2');
// создаём объект
const cube = new Cube(50,50,50,200);
// центр фигуры, вокруг него будем выполнять поворот
const t0 = new Point(150,150,150);
// удалённость центра проекции
const d = 300;
// положение экрана наблюдателя
const tv = new Point(150,150,80);
// угол поворота в градусах
const deg = {x:0,y:1,z:0};
```
```js
// поворот фигуры и обновление изображения
function repaint() {
  cube.rotate(deg, t0);
  // рисуем параллельную проекцию
  drawFigure(canvas1, cube.projection('parallel', tv));
  // рисуем перспективную проекцию
  drawFigure(canvas2, cube.projection('perspective', tv, d));
}
```
```js
// рисуем фигуру по точкам из массива
function drawFigure(canvas, proj) {
  let context = canvas.getContext('2d');
  // сортируем грани по их наклону
  proj.sort((a,b) => b.clock-a.clock);
  // очищаем весь холст целиком
  context.clearRect(0, 0, canvas.width, canvas.height);
  // обходим массив граней куба
  for (let i = 0; i < proj.length; i++) {
    // обходим массив точек и соединяем их линиями
    context.beginPath();
    for (let j = 0; j < proj[i].length; j++) {
      if (j == 0) {
        context.moveTo(proj[i][j].x, proj[i][j].y);
      } else {
        context.lineTo(proj[i][j].x, proj[i][j].y);
      }
    }
    context.closePath();
    // рисуем грань куба вместе с рёбрами
    context.lineWidth = 2.2;
    context.lineJoin = 'round';
    context.fillStyle = '#fff9';
    context.strokeStyle = '#222';
    context.fill();
    context.stroke();
  }
}
```
```js
// после загрузки страницы, задаём частоту обновления изображения 20 Гц
document.addEventListener('DOMContentLoaded',()=>setInterval(repaint,50));
```
